package edu.cmich.cps.cps240.partyprincesspalace.hw06;

import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Scanner;
import java.util.Vector;



import javax.swing.JFileChooser;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

public class Workspace extends JPanel {
	private static final long serialVersionUID = -6964182349722873305L;
	private JPopupMenu chipMenu;
	private JMenuItem setNameMenuItem;
	private JMenuItem removeMenuItem;
	private ChipMenuListener chipPopupListener;
	
	private Vector<Chip> chips;
	private Vector<Wire> wires;
	private Chip chiptemp;
	private Vector<InputBus> inputs;
	private Vector<OutputBus> outputs;
	private Chip chipBeingDragged;
	private Wire tempWire;
	private Point oldMouseLocation;
	private boolean dragging;
	private JFileChooser fc = new JFileChooser();
	private CreateHDLFile chf = null;
	private static int gridSquareSize = 10;
	
	//true if the workspace should create wires with mouse click/drag events
	//false if not
	private boolean wireCreation;
	//true if the workspace is adding a new segment to an existing wire during
	//wireCreation mode
	private boolean addingSegment;
	
	/**
	 * Initializes the workspace's collections of chips, wires, inputs and outputs
	 */
	public Workspace(){
		super();
		this.addMouseListener(getChipPopupListener());
		chips = new Vector<Chip>();
		wires = new Vector<Wire>();
		inputs = new Vector<InputBus>();
		outputs = new Vector<OutputBus>();
	}
	
	/**
	 * Changes the "drag" state of the workspace
	 * @param isDrag true if the workspace has started dragging <br />
	 * false if the drag operation has been completed
	 */
	private void setDrag(boolean isDrag)
	{
		dragging = isDrag;
	}
	
	/**
	 * This method returns the state of the dragging value
	 * @return dragging Boolean determines if a chip is being dragged or not
	 */
	public boolean getDrag(){
		return dragging;
	}
	
	/**
	 * Draws the circuit that we are building
	 */
	@Override
	public void paint(Graphics g)
	{
		super.paint(g);
		
		//Draw some pixels to make up our grid
		for (int i = 0; i < this.getSize().width; i=i+10)
		{
			for(int j = 0; j<this.getSize().height; j=j+10)
			{
				g.drawOval(i, j, 0, 0);
			}
		}

		//Draw wires first, because it will be easier to draw connected pins
		//from the chip, and we'll want this visualization to be drawn "over"
		//the wires
		for (Wire iter : wires)
		{
			iter.draw(g);
		}
		
		for (Chip iter : chips)
		{
			iter.draw(g);
		}
		
		for (InputBus iter : inputs)
		{
			iter.draw(g);
		}
		
		for (OutputBus iter : outputs)
		{
			iter.draw(g);
		}
		
		if (tempWire != null)
		{
			tempWire.draw(g);
		}
	}
	
	
	/**
	 * This method controls the Workspace's reaction to being clicked on
	 * @param spot Point which was clicked
	 */
	public void wasClicked(Point spot){
		
		if(getChip() != null){
			spot = snapToGrid(spot);
			addSelectedChip(spot);
			setChip(null); // What about the selected element in the toolbox?
		}					// Does the toolbox currently have the functionality to deselect something?
							// Or do you mean you want to be able to just keep clicking and adding the
							// same thing over and over again?
		else
		{
			
		}
		
	}
	
	
	/**
	 * This method checks to see if a chip was hit and returns the chip that was hit
	 * 
	 * @param spot Point to check is any chip is there
	 * @return Chip that was hit, else null
	 */
	public Chip hitChip(Point spot){
		for(int i = 0; i < chips.size(); i++){
			if (chips.get(i).isClose(spot)) return chips.get(i);
		}
		
		for (InputBus input : inputs)
		{
			if (input.isClose(spot)) return input;
		}
		
		for (OutputBus output : outputs)
		{
			if (output.isClose(spot)) return output;
		}
		return null;
	}
	
	/**
	 * This method accepts a point and sets variables to prepare to drag the chip selected
	 * 
	 * @param place Point to check for offset
	 */
	public void startDragging(Point place){
		//If we are in wire drawing mode, create a new wire instead of
		//messing with chips
		if (wireCreation)
		{
			this.startWire(place);
			return;
		}
		
		chipBeingDragged = hitChip(place);
		if(chipBeingDragged != null)
		{
			oldMouseLocation = new Point(place);
		}
	}
	
	/**
	 * This method accepts a point and updates the location of the chip being dragged relative to it
	 *  
	 * @param dragLocation Point from mouseDrag event
	 */

	public void dragging(Point dragLocation){
		setDrag(true);
		if (wireCreation)
		{
			this.dragWire(dragLocation);
			return;
		}
		
		if (chipBeingDragged != null && dragLocation != null)
		{
			Point mouseDelta = new Point(oldMouseLocation.x - dragLocation.x,
					oldMouseLocation.y - dragLocation.y);
			Point chiploc = chipBeingDragged.returnLocation();
			
			chiploc.x -= mouseDelta.x;
			chiploc.y -= mouseDelta.y;
			
			chipBeingDragged.move(chiploc);
			
			oldMouseLocation.x = dragLocation.x;
			oldMouseLocation.y = dragLocation.y;
	    	repaint();
		}
	}
	
	/**
	 * To be called when the user releases a mouse button for dragging
	 * events.
	 * @param dragLocation The last location of the mouse in the drag event
	 */
	public void endDrag(Point dragLocation)
	{
		if (wireCreation)
		{
			endWire(dragLocation);
			
		}
		else
		{
			//Chip dragging
			if(oldMouseLocation != null && chipBeingDragged != null)
			{
				Point mouseDelta = new Point(oldMouseLocation.x - dragLocation.x,
						oldMouseLocation.y - dragLocation.y);
				Point chiploc = chipBeingDragged.returnLocation();
				
				chiploc.x -= mouseDelta.x;
				chiploc.y -= mouseDelta.y;
				chiploc = snapToGrid(chiploc);
				chipBeingDragged.move(chiploc);
				chipBeingDragged = null;
				repaint();
			}
		}
		setDrag(false);
	}
	
	public static int getGridSquareSize(){
		return gridSquareSize;
	}
	
	/**
	 * This method accepts a point and sets it to the closest point of a 10x10 grid
	 * 
	 * @param spot Point to be changed
	 * @return
	 */
	public static Point snapToGrid(Point spot){
		int oldX = (int)spot.getX();
		int oldY = (int)spot.getY();
		
		int newX = (int)Math.floor((oldX + (getGridSquareSize()/2.0)) / getGridSquareSize()) * getGridSquareSize();
		int newY = (int)Math.floor((oldY + (getGridSquareSize()/2.0)) / getGridSquareSize()) * getGridSquareSize();
		Point snapTo = new Point(newX, newY);
		return snapTo;
	}
	
	
	/**
	 * This method will add the temporary chip handed to us by the
	 * toolbox at the specified point in the workspace (to be called
	 * from a mouse click event)
	 * 
	 * @param spot The point where the chip will be added
	 */
	
	private void addSelectedChip(Point spot)
	{
		Chip chip = getChip();
		chip.move(spot);
		if (chip instanceof InputBus)
		{
			inputs.add((InputBus) chip);
		}
		else if (chip instanceof OutputBus)
		{
			outputs.add((OutputBus) chip);
		}
		else
		{
			chips.add(chip);
		}
		setChip(null);
		repaint();
	}
	
	// Should the following two methods refer to chiptemp specifically? Spencer
	// getChip() could actually be made private, but unless we want to verify that chipIn is a valid chip,
		// then all we'd be doing is creating a second method that looks just like it, and having a different
		// interface method which accepts the chip and passes it to it, unless I'm misunderstanding you.
		// Something like:
		// private void setChip(Chip chipIn){
		//		chiptemp = chipIn;
		//	}
		//
		// public void setTempChip(Chip chipIn){
		//	setChip(chipIn);
		//	}
		// Is that what you're suggesting?
	/**
	 * Sets the workspace up for adding a new chip to itself.
	 * @param chipIn the next chip that we will be adding to the workspace. <br />
	 * Set to null if you want prevent the workspace from adding a chip.
	 */
	public void setChip(Chip chipIn){
		chiptemp = chipIn;
	}
	
	/**
	 * @return the chip that we will be adding to the workspace.
	 */
	public Chip getChip(){
		return chiptemp;
	}
	
	/**
	 * Wire logic which gets called on mouse pressed events when the workspace is in
	 * wire creation/manipulation mode.  We might create a new wire, or we might modify
	 * an existing wire based on 
	 * @param location The location where the mouse button was pressed down
	 */
	private void startWire(Point location)
	{
		if (wireCreation)
		{
			//If the point can "snap to" an existing wire, then work on
			//adding a new segment to an existing wire instead of creating
			//a brand new wire
			for (Wire wire : wires)
			{
				Point2D temp;
				temp = wire.snapToNearBySegmentPoint(location);
				if (temp != null)
				{
					tempWire = wire;
					addingSegment = true;
					tempWire.attemptAddSegment(snapToGrid(location), snapToGrid(location));
					return;
				}
			}
			tempWire = new Wire(snapToGrid(location));
			if (attemptAttach(location) == true)
			{
				System.out.println("Attached at start!");
			}
			tempWire.selectSegmentNearPoint(location);
		}
	}
	
	/**
	 * Changes how the workspace treats mouse events.  Normally, it manipulates chips
	 * but if the user selects the wire tool, then the wire tool uses this function to
	 * tell the workspace to create and manipulate wires instead.
	 * @param state true if the workspace is to manipulate wires, else false
	 */
	public void setWireTool(boolean state)
	{
		wireCreation = state;
	}
	
	/**
	 * Mouse drag event handler for the wire creation process.
	 * @param location the location of the mouse where the dragging is occuring
	 */
	private void dragWire(Point location)
	{
		if (wireCreation && tempWire != null)
		{
			tempWire.extendSelectedSegment(location);
		}
	}
	
	/**
	 * Mouse button released event handler for the wire creation process
	 * @param location The location of the mouse where the button was released
	 */
	private void endWire(Point location)
	{
		if (wireCreation && tempWire != null)
		{
			System.out.println("Ending!");
			if (this.getDrag() == false)
			{
				tempWire = null;
				return;
			}
			Point2D tmpLoc = tempWire.extendSelectedSegment(snapToGrid(location));
			Point realLoc = new Point((int)tmpLoc.getX(), (int)tmpLoc.getY());
			
			if (addingSegment)
			{
				addingSegment = false;
			}
			else
			{
				//If we aren't adding a segment to an existing wire, that means that we added a new wire
				wires.add(tempWire);
			}
			
			//If the location is close to a ChipPin, then attach the wire
			if (attemptAttach(realLoc) == true)
			{
				System.out.println("Attached at end!");
			}
			tempWire = null;
			repaint();
		}
	}
	
	/**
	 * Attempts to attach our tempWire (the wire currently being modified)
	 * to a ChipPin located close to the given point.  The given point should
	 * be the last point when dragging a wire
	 * @param location A point around which to search for candidates to make a connection
	 * @return true if we successfully connected to something, else false
	 */
	private boolean attemptAttach(Point location)
	{
		for (Chip chip : chips)
		{
			ChipPin cpin = chip.getNearbyPin(location);
			if (cpin != null)
			{
				tempWire.attachTo(cpin);
				return true;
			}
		}
		
		for (InputBus input : inputs)
		{
			ChipPin ipin = input.getNearbyPin(location);
			if (ipin != null)
			{
				tempWire.attachTo(ipin);
				return true;
			}
		}
		
		for (OutputBus output : outputs)
		{
			ChipPin opin = output.getNearbyPin(location);
			if (opin != null)
			{
				tempWire.attachTo(opin);
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Initializes and/or returns the popup menu for when chips are clicked
	 */
	private JPopupMenu getChipMenu()
	{
		if (chipMenu == null)
		{
			chipMenu = new JPopupMenu();
			chipMenu.add(getSetNameMenuItem());
			chipMenu.add(getRemoveMenuItem());
			
			chipMenu.addPopupMenuListener(getChipPopupListener());
		}
		return chipMenu;
	}
	
	/**
	 * Initializes and/or returns the popup menu item for when a chip's name
	 * is to be modified (if possible)
	 */
	private JMenuItem getSetNameMenuItem()
	{
		if (setNameMenuItem == null)
		{
			setNameMenuItem = new JMenuItem("Set name");
			setNameMenuItem.addActionListener(getChipPopupListener());

		}
		return setNameMenuItem;
	}
	
	/**
	 * Initializes and/or returns the popup menu item for when a chip is to
	 * be removed
	 */
	private JMenuItem getRemoveMenuItem()
	{
		if (removeMenuItem == null)
		{
			removeMenuItem = new JMenuItem("Remove");
			removeMenuItem.addActionListener(getChipPopupListener());
		}
		return removeMenuItem;
	}
	
	private ChipMenuListener getChipPopupListener()
	{
		if (chipPopupListener == null)
		{
			chipPopupListener = new ChipMenuListener();
		}
		return chipPopupListener;
	}
	
	class ChipMenuListener extends MouseAdapter implements ActionListener, PopupMenuListener
	{
		WorkspaceObj selected;
		public void mousePressed(MouseEvent e)
		{
			maybePopup(e);
		}
		
		public void mouseReleased(MouseEvent e)
		{
			maybePopup(e);
		}
		
		private void maybePopup(MouseEvent e)
		{
			if (e.isPopupTrigger())
			{
				selected = hitChip(e.getPoint());
				if (selected != null)
				{
					getChipMenu().show(e.getComponent(), e.getX(), e.getY());
				}
				for (Wire wire : wires)
				{
					if (wire.isClose(e.getPoint()))
					{
						selected = wire;
						getChipMenu().show(e.getComponent(), e.getX(), e.getY());
					}
				}
				
				if (selected instanceof InputBus || selected instanceof OutputBus
						|| selected instanceof Wire)
				{
					getSetNameMenuItem().setEnabled(true);
				}
				else
				{
					getSetNameMenuItem().setEnabled(false);
				}
			}
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			if (e.getSource() == getSetNameMenuItem())
			{
				try
				{
					selected.setName(JOptionPane.showInputDialog(null, "Input the new name for the object"));
					repaint();
					selected = null;
				}
				catch (NullPointerException exce)
				{
					System.err.println(exce.getMessage());
				}
			}
			else if (e.getSource() == getRemoveMenuItem())
			{
				if (selected == null)
				{
					return;
				}
				selected.remove();
				
				if (chips.remove(selected))
				{
					
				}
				else if (inputs.remove(selected))
				{
					
				}
				else if (outputs.remove(selected))
				{
					
				}
				else if (wires.remove(selected))
				{
					
				}
				repaint();
			}
		}

		@Override
		public void popupMenuCanceled(PopupMenuEvent arg0) {
			selected = null;
			
		}

		@Override
		public void popupMenuWillBecomeInvisible(PopupMenuEvent arg0) {
			//Do nothing
		}

		@Override
		public void popupMenuWillBecomeVisible(PopupMenuEvent arg0) {
			//Do nothing
		}
	}
	
	/**
	 * Hooray for writing comments for methods which you did not write!
	 * Takes the current circuit and saves it to a file using object
	 * serialization to write data out in raw binary.
	 */
	public void projectToFile() {
		fc.showSaveDialog(null);
		System.out.println(fc.getSelectedFile());
		FileOutputStream fos = null;
		ObjectOutputStream oos = null;
		
		try {
			fos = new FileOutputStream(fc.getSelectedFile());
			oos = new ObjectOutputStream(fos);
			
			for (int i = 0; i < wires.size(); i++) {
				oos.writeObject((Wire)wires.get(i));
			}
			
			for (int i = 0; i < chips.size(); i++) {
				oos.writeObject((Chip)chips.get(i));
			}
			
			for (int i = 0; i < inputs.size(); i++) {
				oos.writeObject((InputBus)inputs.get(i));
			}
			
			for (int i = 0; i < outputs.size(); i++) {
				oos.writeObject((OutputBus)outputs.get(i));
			}
		} catch (IOException e) {
			System.out.println(e);
		} finally{
			try {
				oos.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		
	}
	
	/**
	 * Opens a file dialogue and then loads the given file.  The file
	 * should have been writen using the projectToFile() function above.
	 * Uses object serialization to restore data.
	 */
	public void fileToProject(){
		fc.showOpenDialog(null);
		
		//temporary storage as we load new data
		Vector<Wire> newWires = new Vector<Wire>();
		Vector<Chip> newChips = new Vector<Chip>();
		Vector<InputBus> newInputs = new Vector<InputBus>();
		Vector<OutputBus> newOutputs = new Vector<OutputBus>();
		
		try{
			FileInputStream fis = new FileInputStream(fc.getSelectedFile());
			ObjectInputStream ois = new ObjectInputStream(fis);
			while(ois != null){
				Object obj = ois.readObject();
				
				if(obj instanceof Wire){
					newWires.add((Wire) obj);
				}
				else if(obj instanceof Chip){
					newChips.add((Chip) obj);
				}
				else if(obj instanceof InputBus){
					newInputs.add((InputBus) obj);
				}
				else if(obj instanceof OutputBus){
					newOutputs.add((OutputBus) obj);
				}
			}			
			fis.close();
			
		} catch (EOFException e){
			//Why does Java only allow end of file detection through exceptions for binary mode file reading?
			//Exceptions should be EXCEPTIONAL, not expected.
			
		} catch (FileNotFoundException e) {
			//We have to return immediately, or else we lose all data
			return;
		} catch (IOException e) {
			//Return immediately so we don't corrupt our old data
			return;
		} catch (ClassNotFoundException e) {
			//Return immediately
			return;
		}
		
		wires = newWires;
		chips = newChips;
		inputs = newInputs;
		outputs = newOutputs;
		tempWire = null;
		dragging = false;
		addingSegment = false;
		wireCreation = false;
		chiptemp = null;
		repaint();
	}

	/**
	 * Exports the circuit to an HDL file, chosen by the user.
	 */
	void projectToHDL(){
		fc.showSaveDialog(null);
		System.out.println(fc.getSelectedFile());
		try {
			chf = new CreateHDLFile(fc.getSelectedFile());
			chf.create(inputs,outputs,chips,wires);
			
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/*
	 * Code was not completed on time.  I am commenting this out to prevent side effects
	 * It was also not commented at all
	public void HDLtoCChip(){
		fc.showOpenDialog(null);
		int ins = 0;
		int outs = 0;
		Vector<String> inName = new Vector<String>();
		Vector<String> outName = new Vector<String>();
		String cur;
		try{
			File fis = new File(fc.getSelectedFile().getPath());
			Scanner scan = new Scanner(fis);
			while(scan.hasNext()){
				cur = scan.next();
				if(cur.equals("IN")){
					while(cur.charAt(cur.length()-1) != ';'){
						ins = ins +1;
						inName.add(cur.substring(0, cur.length()-1));
						cur = scan.next();
					}
				}
				
				if(cur.equals("OUT")){
					cur = scan.next();
					while(cur.charAt(cur.length()-1) != ';'){
						outs = outs +1;
						outName.add(cur.substring(0, cur.length()-1));
						cur = scan.next();
					}
				}
			}
			Vector<ChipPin> inPins = new Vector<ChipPin>();
			Vector<ChipPin> outPins = new Vector<ChipPin>();
			
			for(int i=0; i<inName.capacity(); i++){
				
			}
			
			}
		catch(IOException e){
			System.out.println(e);
		}
	}
	*/
}